Skip to content

feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code) - #4625

Draft
wpfleger96 wants to merge 5 commits into
mainfrom
duncan/effort-write-genericize
Draft

feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code)#4625
wpfleger96 wants to merge 5 commits into
mainfrom
duncan/effort-write-genericize

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Genericizes the agent effort write-side so non-Buzz runtimes (Goose and Claude Code) participate in the same canonical effort contract as buzz-agent, with a spawn bridge that translates the canonical key to whatever the target harness expects at launch time.

Five commits on top of main (cb4a73e17).


Phase 1 — B-collapse + ACP identity + alias normalization

desktop/src-tauri/src/managed_agents/config_bridge/

  • B-collapse for same-value live effort. Previously a live thought_level option whose raw value equalled the global default was reported as a live override instead of collapsing to GlobalDefault. Fixed: compare via normalized values after applying effort_norm.
  • thought_level ACP identity. When both thought_level and thinking_effort ACP categories are present, thought_level wins. The winning entry's config_id is captured and forwarded to write_via; it is always entry-supplied, never hardcoded.
  • ACP effort normalization. The selected ACP option's value is normalized through effort_norm before candidate-list construction and B comparison. An invalid ACP value (e.g. minimal for Goose) is omitted as a value candidate so a lower tier wins; write_via stays AcpSetConfigOption with the entry-supplied config_id because the route targets the option, not its current value.
  • Alias normalization (TS). normalizeEffortValue in buzzAgentConfig.ts uses the runtime-supplied effortAliases descriptor as the single normalization authority. Absent effortAliases means no aliases — the built-in Goose fallback table has been removed. Canonical values pass through; unrecognised values return null.

Phase 2 — Canonical effort contract + native-key persistence + mutation contract

desktop/src/features/agents/

Single read policy. resolveEffortFromEnv(envVars, nativeKey, legacyKey, acceptedValues, effortAliases) in agentConfigCore.ts is the authoritative read path for all effort surfaces. It normalizes, applies native-first / valid-legacy-fallback, and returns { value, legacyConsumed }. deriveAgentConfigFieldModel, HarnessNativeEffortFields, and both editors' legacyEffortConsumed memos all call it.

Descriptor. deriveAgentConfigFieldModel emits an effort field with:

  • optionSource: "harnessNative" for runtimes with a static acceptedEffortValues vocabulary (Goose, Claude Code); "buzzAgentCatalog" for buzz-agent; "legacyProviderModelCatalog" otherwise.
  • acceptedEffortValues from runtime metadata — never the seven-value buzz-agent list.
  • Legacy fallback (legacyKey = BUZZ_AGENT_THINKING_EFFORT) at record/persona scope only; omitted at global/onboarding (Delta 5: that key is buzz-agent's own native key there).

UI surfaces. AgentConfigFields (global/onboarding), PersonaAdvancedFields (persona/definition), and EditAgentAdvancedFields (record/instance) all branch on optionSource === "harnessNative". For harness-native runtimes they render HarnessNativeEffortFields with runtime metadata; EffortSelectField receives canonical choices from acceptedEffortValues, not the Buzz catalog.

HarnessNativeEffortFields. Accepts legacyEnvKey: string | null (required — null at global/onboarding enforces the tier boundary and prevents silent legacy key deletion). Reads via resolveEffortFromEnv. Save = canonical native write + atomic legacy delete at record/persona scope; native-key-only at global scope (preserves coexisting foreign native keys — Delta 5).

applyHarnessNativeEffortChange. Exported production mutation helper called by both the component and AgentConfigFields's onChange. Encodes the scope-split policy: legacyKey = BUZZ_AGENT_THINKING_EFFORT at record/persona; legacyKey = null at global. Tests import and exercise it directly.

Model-coupled mutation guard. Heal-on-mount, orphan-clear, and useEffortAutoClear in AgentConfigFields skip the native effort key when isHarnessNativeEffort. Goose and Claude Code effort is model-independent — their vocabularies are static; the reader's skip-as-absent contract handles invalid values non-destructively. The useEffortAutoClear hook receives "" as currentEffort for harness-native runtimes, making it a no-op.

Inherit-transition strip. resolveInheritedRuntimeSubmission accepts excludePersonaEnvKeys to strip effort keys from the persona merge layer on a pinned→inherit transition. AgentInstanceEditDialog passes [thinkingEnvVar, ...allKnownEffortKeys(runtimes)] when the runtime has thinkingEnvVar — derives the known-key set from the catalog, not a static constant.

Legacy row visibility. legacyConsumedKey controls advanced-row suppression. The legacy row is hidden only when the key was validly consumed; invalid or unrecognised legacy values remain visible as advanced env rows.


Phase 3 — Shared alias resolver + spawn effort bridge + deploy test

desktop/src-tauri/

effort_tier_alias. managed_agents/config_bridge/mod.rs exposes effort_tier_alias(value, accepted_values, allow_legacy_alias) — the shared normalizer used by build_thinking_field and apply_effort_bridge. The boolean parameter is allow_legacy_alias with an explicit per-tier doc table (record: true, persona: true, global: false, definition: false, baked: false) so no tier can silently diverge.

apply_effort_bridge. managed_agents/config_bridge/mod.rs bridges canonical effort onto the spawn payload:

  • Strips all known foreign effort keys using all_known_effort_keys() (derived from KNOWN_ACP_RUNTIMES::thinking_env_var, not a static constant).
  • Resolves the canonical winner from all tiers: record native → record legacy → persona native → persona legacy → global native → definition native → baked native.
  • Baked tier is native-key-only (no legacy alias); baked xhigh normalizes to max; invalid baked values skip as absent.
  • Definition tier uses allow_legacy_alias=false — mirrors the reader's contract (plan v3 Delta 2).
  • Writes to launch_config.launch.env for deploy/cloud agents; top-level env_vars is left unbridged.

One metadata contract. EffortNormalization.aliases is serialized through AcpRuntimeCatalogEntry.effort_aliases in discovery.rs and surfaced through RawAcpRuntimeCatalogEntry / AcpRuntimeCatalogEntry in TS. normalizeEffortValue uses the descriptor-supplied alias table; absent effortAliases means no aliases (not Goose aliases). All production call sites pass the descriptor: AgentConfigFields via resolveBakedEffort, both dialogs via useAgentDialogDefaults(runtime: ...), HarnessNativeEffortFields via normalizeEffortValue(raw, acceptedEffortValues, effortAliases). The static ALL_KNOWN_EFFORT_KEYS constant (Rust and TS) is replaced by all_known_effort_keys() / allKnownEffortKeys(runtimes) derived from runtime declarations — adding a runtime automatically participates in foreign-key stripping without touching a constant.

Baked-effort display. resolveBakedEffort(bakedEnv, nativeKey, runtime) in bakedEnvHelpers.ts is the shared resolver for every baked-effort display surface: AgentConfigFields (global/onboarding) and getInheritedAgentDefaults. Both normalize through alias→canonical / invalid→absent so display parity with spawn is maintained. useAgentDialogDefaults gains a runtime param; callers pass runtime: runtimes.find(...) or runtime: prospectiveRuntime in one line rather than three separate props.


Phase 4 — Claude Code effort enablement

desktop/src-tauri/src/managed_agents/discovery/ + desktop/src/features/agents/lib/

Catalog declaration. CLAUDE_EFFORT_NORMALIZATION added in runtime_metadata.rs with canonical vocabulary low | medium | high | xhigh | max and no aliases (Claude Code accepts these natively; xhigh is canonical, not an alias as it is for Goose). The auto value is deliberately excluded — unset env already means model-default. Available levels are per-model, but Claude Code falls back to the nearest supported level, so a static vocabulary declaration is safe.

Catalog wiring. The claude entry in KNOWN_ACP_RUNTIMES now declares thinking_env_var: Some("CLAUDE_CODE_EFFORT_LEVEL") and effort_normalization: Some(&CLAUDE_EFFORT_NORMALIZATION). The env var is inherited by the claude-agent-acp Node.js adapter process from Buzz's spawn env, which passes it through process.env to the underlying SDK subprocess env and then to the claude CLI binary.

UI policy removal. The else if (runtime?.id === "claude") deferral branch (render: "deferredUntilNativeOptionsAvailable") in agentConfigCore.ts is removed. Claude now hits the existing if (runtime?.thinkingEnvVar) path and emits a harnessNative descriptor with render: "control" — zero new UI code.


Test coverage

  • Rust: ACP B-collapse, thought_level priority, ACP alias/case/invalid normalization, alias resolver tiers, bridge descriptor hygiene, deploy parity boundary; baked tier (high/xhigh→max/minimal→absent/legacy-excluded/beaten-by-record); definition tier (legacy excluded, native accepted) in both spawn and reader; Claude Code catalog vocabulary/no-alias pins, spawn native survival, invalid-value skip, foreign-key stripping, legacy alias at record tier (valid and invalid-for-Claude cases).
  • TS: resolveEffortFromEnv read policy (native-first, legacy fallback, tier boundary, null-legacyKey); applyHarnessNativeEffortChange scope-split mutation; deriveAgentConfigFieldModel at global/definition/instance scope; HarnessNativeEffortFields legacy migration; excludePersonaEnvKeys inherit-transition strip; isHarnessNativeEffort guard (xhigh survives model-heal; minimal survives; buzz-agent auto-clear unchanged); inherited-defaults normalization (global xhigh→max, baked xhigh→max, baked minimal→absent); global coexisting native-pair preservation; resolveBakedEffort direct pins (xhigh→max, minimal→null — the production helper used by global/onboarding); normalizeEffortValue absent-aliases pin and non-Goose ultra→max pin (TS normalization comes from supplied descriptor, not any runtime-specific table); Claude Code harnessNative descriptor shape, valid values pass-through, Goose aliases absent from Claude vocab.

Deferred

  • useAgentDialogDefaults retains three unused optional inputs (nativeEffortKey, acceptedEffortValues, effortAliases) superseded by the runtime param. Follow-up cleanup: remove them and require runtime.

@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from 2d8f97a to e9e8f6a Compare August 4, 2026 01:36
@wpfleger96 wpfleger96 changed the title feat(config-bridge): persist Goose effort under native key + spawn legacy bridge feat(agents): harness-agnostic effort write-side + spawn legacy bridge Aug 4, 2026
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch 6 times, most recently from bb1b659 to c93e1c4 Compare August 4, 2026 17:35
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 4 commits August 4, 2026 14:12
…entity + alias normalization (Phase 1)

- EffortNormalization struct in runtime_metadata.rs with canonical values,
  aliases, and normalize_str(); GOOSE_EFFORT_NORMALIZATION static cited to
  goose thinking.rs:277-308 (canonical: off/low/medium/high/max; aliases:
  none|disabled→off, med→medium, xhigh→max)
- effort_normalization field on KnownAcpRuntime; accepted_effort_values on
  AcpRuntimeCatalogEntry (serialized to TS as the single canonical authority)
- LEGACY_THINKING_EFFORT_KEY and ALL_KNOWN_EFFORT_KEYS constants in
  config_bridge/mod.rs; effort_tier_alias() for per-tier native+legacy key
  resolution with normalization and global-tier exclusion
- find_effort_config_option() identifies live effort by ACP category
  thought_level (Goose/claude-agent-acp); falls back to legacy effort
  category for transition compatibility; thought_level always wins when both
  present; returns (value, config_id) so write-route uses the real entry id
- build_thinking_field() normalizes all candidates before precedence,
  override tracking, and B-equality; B-collapse compares normalized ACP
  value against normalized non-ACP winner — none↔off and xhigh↔max collapse
  correctly; write_via uses actual matched config_id (not hardcoded effort)
- Test hygiene: test_runtime()/buzz_agent_runtime() use catalog lookups;
  6 goose_runtime() tests wrapped with with_no_goose_config(); 4 readiness
  stubs gain effort_normalization: None
- 6 new tests in reader_tests_ext.rs pin real Goose ACP shape, both-present
  priority, legacy fallback, alias B-collapse (none↔off, xhigh↔max), and
  env alias normalization

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tation contract (Phase 2)

- acceptedEffortValues on AcpRuntimeCatalogEntry: Goose carries canonical
  off|low|medium|high|max (source: goose thinking.rs:277-308); null for
  buzz-agent (per-model catalog). Single authority for UI, spawn bridge,
  and reader.
- normalizeEffortValue() in buzzAgentConfig.ts: aliases none|disabled→off,
  med→medium, xhigh→max (case-insensitive); null acceptedValues = pass-through
  (buzz-agent path unchanged).
- ALL_KNOWN_EFFORT_KEYS in buzzAgentConfig.ts mirrors Rust constant; used for
  record/persona runtime-transition cleanup.
- agentConfigCore.ts: native key wins; legacy-only pre-migration save shown via
  effortValue fallback; legacyConsumedKey suppresses duplicate advanced row.
  optionSource harnessNative when acceptedEffortValues present.
- AgentConfigFields.tsx: descriptor-driven global/onboarding path for
  harnessNative runtimes (isHarnessNativeEffort): canonical options from
  acceptedEffortValues, currentEffort from normalised descriptor value,
  effortValidForAutoClear from acceptedEffortValues. Unconditional
  BUZZ_AGENT_THINKING_EFFORT deletes guarded by !isHarnessNativeEffort (Delta 5:
  global-scope coexistence). allStructuredKeys includes legacyConsumedKey;
  effort clear deletes native + legacy; bakedStructuredKeys() replaces static set.
- buzzAgentModelTuningFields.tsx: applyHarnessNativeEffortChange() exported
  production helper — single spec for the mutation contract. legacyKey=null at
  global/onboarding scope (Delta 5), legacyKey=BUZZ_AGENT_THINKING_EFFORT at
  record/persona scope. EffortSelectField gains canonicalValues prop to bypass
  Buzz 7-value catalog. HarnessNativeEffortFields calls production helper.
- personaRuntimeModel.ts: resolveInheritedRuntimeSubmission gains
  excludePersonaEnvKeys param. Strips harness-native effort keys from persona
  layer on inherit-transition so the record never acquires a frozen copy of
  persona effort (effort inherits at spawn; it is not a credential).
- AgentInstanceEditDialog.tsx: passes excludePersonaEnvKeys with runtime's
  thinkingEnvVar + ALL_KNOWN_EFFORT_KEYS when runtime has acceptedEffortValues.
- bakedEnvHelpers.ts: getInheritedAgentDefaults accepts nativeEffortKey option;
  baked lookup is native-only for non-buzz-agent runtimes (Delta-4 boundary).
  bakedStructuredKeys() exported for AgentConfigFields.
- useAgentDialogDefaults.ts: accepts nativeEffortKey; seeds inherited env under
  native key.
- AgentDefinitionDialog + AgentInstanceEditDialog: pass prev/next native effort
  keys to selectionOnRuntimeChange; handleInheritHarnessChange clears all known
  effort keys on pin↔inherit toggle.
- runtimeModelProviderSelection.ts: selectionOnRuntimeChange clears prev native
  + next native + legacy at record/persona scope; skip when native === legacy
  (buzz-agent same-key invariant).
- agentConfigOptions.tsx: resetConfigForHarnessChange preserves all effort keys
  (global-scope Delta-5 rule; each runtime's descriptor strips foreign keys).
- agent_config.rs: is_safe_to_reveal derives allowed effort keys from
  ALL_KNOWN_EFFORT_KEYS so GOOSE_THINKING_EFFORT is unmasked at baked tier.
- AGENTS.md rule-2 'until PR 2.7' clause removed: native-key persistence lands
  in this commit.
- Test coverage: applyHarnessNativeEffortChange record/persona + global scope
  pins; resolveInheritedRuntimeSubmission inherit-no-materialize (native +
  legacy), concrete-override-wins, non-effort-merged, full-submit-payload-clean;
  global Goose descriptor optionSource/value/invalid/no-legacy pins;
  runtimeModelProviderSelection useLiteralKeys cleanup.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…st (Phase 3)

- apply_effort_bridge() in config_bridge/mod.rs: applied after merged user env
  in resolve_effective_agent_env_with_def. Strips all foreign known effort keys
  (runtime-scoped invariant), then resolves canonical effort via tier-first
  precedence (record native → record legacy → persona native → persona legacy
  → global native → definition native; global legacy excluded end-to-end).
  Removes raw native key (possibly alias-form) from env before inserting
  canonical winner — skip-as-absent applies to the output map, not just winner
  selection.
- effort_tier_alias() with global_tier=true: honours native key only at global
  tier; legacy alias excluded there (plan v3 Delta 2).
- readiness_effort_bridge_tests.rs: 12 spawn tests covering bridge activation,
  tier-first precedence (record beats persona, record legacy beats persona
  native, global legacy excluded), alias normalization (none→off, xhigh→max),
  invalid-skip-as-absent (minimal skipped, persona native wins), and the
  bidirectional global coexist invariant (Goose and buzz-agent from same global
  config each receive only their own native key; GOOSE_THINKING_EFFORT stripped
  from buzz-agent descriptor, BUZZ_AGENT_THINKING_EFFORT stripped from Goose).
- agents_deploy.rs: deploy parity test pins that a legacy-only Goose record
  produces GOOSE_THINKING_EFFORT in launch.env while top-level env_vars retains
  the legacy key as compatibility bookkeeping (Delta-4 contract boundary).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… one metadata contract

Thufir's review identified three IMPORTANT defects (all live display/spawn
divergences — the same class LP-EFFORT-VISIBILITY targets):

1. Baked native effort deleted at spawn while the UI reported it as inherited.
   Rust: apply_effort_bridge now carries baked_env as the lowest-precedence tier
   (native-key only, no legacy alias). TS: resolveBakedEffort extracted as a
   shared helper in bakedEnvHelpers.ts; AgentConfigFields (global/onboarding)
   now routes through it so baked xhigh→max and invalid minimal→absent on both
   display and spawn. Pins: resolveBakedEffort_{xhigh_normalizes_to_max,
   invalid_minimal_returns_null}.

2. Definition-tier alias-policy inversion: spawn passed allow_legacy_alias=false
   correctly at global but incorrectly at definition. The bool param is renamed
   to allow_legacy_alias with an explicit doc table; global and definition cannot
   silently diverge again.

3. Metadata contract duplicated across Rust and TS: EffortNormalization.aliases
   serialized through AcpRuntimeCatalogEntry.effort_aliases; normalizeEffortValue
   fallback alias table removed — absent effortAliases means no aliases (not
   Goose aliases). effortAliases threaded through all production call sites
   (AgentDefinitionDialog, AgentInstanceEditDialog, HarnessNativeEffortFields).
   useAgentDialogDefaults gains a runtime param so callers pass one object
   instead of three; AgentDefinitionDialog/AgentInstanceEditDialog use it.
   ALL_KNOWN_EFFORT_KEYS static constant removed; allKnownEffortKeys() derives
   from runtime declarations in both Rust and TS. Fixtures in agentConfigCore
   and buzzAgentConfig tests updated to carry effortAliases; non-Goose
   ultra→max pin proves normalization comes from the descriptor, not a table.

Rust ALL_KNOWN_EFFORT_KEYS → all_known_effort_keys() (derived from
KNOWN_ACP_RUNTIMES); apply_effort_bridge: #[allow(clippy::too_many_arguments)]
added for the 8th param.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from c93e1c4 to febf701 Compare August 4, 2026 18:32
@wpfleger96 wpfleger96 changed the title feat(agents): harness-agnostic effort write-side + spawn legacy bridge feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code) Aug 4, 2026
Claude Code exposes CLAUDE_CODE_EFFORT_LEVEL (low|medium|high|xhigh|max)
as a first-class env var that the claude-agent-acp adapter inherits from
its parent process and passes through to the SDK subprocess env. Adding
Claude to the catalog contract is therefore a pure catalog declaration —
no new UI logic, no per-harness special casing.

Changes:
- Add CLAUDE_EFFORT_NORMALIZATION static with 5-value vocabulary and no
  aliases (Claude accepts values natively; xhigh is canonical, not an
  alias as it is for Goose).
- Wire thinking_env_var and effort_normalization into the claude entry in
  KNOWN_ACP_RUNTIMES; import CLAUDE_EFFORT_NORMALIZATION in discovery.rs.
- Remove the Claude deferral branch (render: deferredUntilNativeOptions-
  Available) from agentConfigCore.ts — Claude now hits the existing
  harnessNative path just like Goose.
- Update runtimeModelProviderSelection.ts comment: buzz→claude now fires
  the else-if cleanup branch; claude→goose fires the if branch.
- Update AGENTS.md rule 3: Claude effort is no longer deferred.
- Rust pins: catalog vocabulary/no-alias contract, spawn native survival,
  invalid-value skip, foreign-key stripping, legacy alias at record tier
  (valid and invalid-for-Claude cases).
- TS pins: valid values pass through, Goose aliases absent from Claude
  vocab, empty alias array means no fallback table.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from 80df98f to 7c4c183 Compare August 4, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant